/*
 * Copyright (c) 2009 by unreserved.org.
 * All rights reserved.
 */

package org.unreserved.util;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.lang.reflect.*;
import java.util.*;
import org.apache.log4j.Logger;

/**
 * Sets JavaBeans properties of a target object. 
 */
public class PropertySetter {
    private static final Logger logger = Logger.getLogger(PropertySetter.class);
    
    protected Object obj;
    protected PropertyDescriptor[] props;
    
    /**
     * Construct a PropertySetter that will set properties of the
     * specified "bean" object through the use of reflection.
     * 
     * @param obj target for which properties are to be set
     */
    public PropertySetter(Object obj) throws PropertySetterException {
        this.obj = obj;
        try {
            BeanInfo bi = Introspector.getBeanInfo(obj.getClass());
            props = bi.getPropertyDescriptors();
        } catch (IntrospectionException ex) {
            throw new PropertySetterException(ex);
        }
    }
    
    public static boolean setProperty(Object obj, String name, String value) {
        try {
            return new PropertySetter(obj).setProperty(name, value);
        } catch (PropertySetterException ex) {
            logger.debug("ignoring exception", ex);
            return false;
        }
    }
    
    /**
     * Set a property on the target Object. This is done by invoking a
     * setter method on the object and typically involves converting
     * the value from String to Integer, Boolean etc.
     * 
     * @param name name of the property
     * @param value String representation of the property value
     * @return true if a setter was successfully invoked, false otherwise
     */
    public boolean setProperty(String name, String value) {
        name = Introspector.decapitalize(name);
        PropertyDescriptor prop = getPropertyDescriptor(name);
        
        if (prop == null) {
            logger.warn("setProperty: no property with name \"" + name +
                        "\" in class " + obj.getClass().getName());
            return false;
        } else {
            try {
                setProperty(prop, name, value);
                return true;
            } catch (PropertySetterException ex) {
                logger.warn("setProperty: failed to set property \"" + name +
                            " in class " + obj.getClass().getName(), ex);
                return false;
            }
        }
    }
    
    protected void setProperty(PropertyDescriptor prop, String name, String value)
        throws PropertySetterException
    {
        Object arg;
        Method setter = prop.getWriteMethod();
        
        if (setter == null) {
            throw new PropertySetterException("No setter for property \"" + name + "\"");
        }
        Class[] paramTypes = setter.getParameterTypes();
        if (paramTypes.length != 1) {
            throw new PropertySetterException("number of parameters for setter is not one");
        }
        
        try {
            arg = convertValue(value, paramTypes[0]);
        } catch (PropertySetterException ex) {
            throw ex;
        } catch (Throwable t) {
            throw new PropertySetterException("conversion of value \"" + value + "\" to type \"" + paramTypes[0] + "\" failed", t);
        }
        logger.debug("setProperty: " + name + "=" + arg);
        try {
            setter.invoke(obj, new Object[] { arg });
        } catch (Exception ex) {
            throw new PropertySetterException(ex);
        }
    }
    
    /**
     * Converts a String value to an object specified by the type
     * parameter. Handles primitive values (String, Long, Integer, and
     * Boolean) and Enums.
     * 
     * <p>XXX: add support for arrays
     */
    protected Object convertValue(String val, Class type)
        throws PropertySetterException
    {
        if (logger.isDebugEnabled()) {
            logger.debug("convertValue: val=[" + val + "] type=" + type.getName());
        }
        //val = val.trim();
        if (String.class.isAssignableFrom(type)) {
            return val;
        } else if (Integer.TYPE.isAssignableFrom(type)) {
            return new Integer(val);
        } else if (Long.TYPE.isAssignableFrom(type)) {
            return new Long(val);
        } else if (Boolean.TYPE.isAssignableFrom(type)) {
            if ("true".equalsIgnoreCase(val) || "on".equalsIgnoreCase(val)) {
                return Boolean.TRUE;
            } else if ("false".equalsIgnoreCase(val) || "off".equalsIgnoreCase(val)) {
                return Boolean.FALSE;
            }
        } else if (Character.TYPE.isAssignableFrom(type)) {
            if (val.length() == 1) {
                return new Character(val.charAt(0));
            }
        } else if (Enum.class.isAssignableFrom(type)) {
        	Method valueOfMethod = null;
            try {
            	valueOfMethod = type.getMethod("valueOf", String.class);
                Object enumVal = valueOfMethod.invoke(obj, val);
                return enumVal;
            } catch (NoSuchMethodException ex) {
                logger.error("Enum without valueOf method", ex);
            } catch (Exception ex) {
                // try again with value converted to all uppercase; if fail, log original exception
                val = val.toUpperCase();
                try {
                	return valueOfMethod.invoke(obj, val);
                } catch (Exception ex2) {
                    logger.error("Enum.valueOf failed, class=" + type + ", value=" + val, ex);
                }
            }
        } else {
            throw new PropertySetterException("unknown type: " + type);
        }
        // shouldn't get here
        return null;
    }
    
    protected PropertyDescriptor getPropertyDescriptor(String name) {
        for (int i = 0; i < props.length; i++) {
            if (name.equals(props[i].getName())) {
                return props[i];
            }
        }
        return null;
    }
}
